package de.erb.erir;

import java.awt.GridLayout;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.io.File;
import java.util.ArrayList;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.PopupFactory;
import javax.swing.UIManager;

import net.cl.util.patterns.ProgressListener;
import net.cl.util.ui.HtmlDialog;
import net.cl.util.ui.PlaceholderTextField;
import net.cl.util.ui.SpeechBubblePopupFactory;
import net.cl.util.values.InputFormatException;
import net.cl.util.values.PercentOrAbsoluteInteger;
import de.erb.erir.model.ConversionProgressEvent;
import de.erb.erir.model.ImageConversionWorker;
import de.erb.erir.view.DefaultProgramLook;
import de.erb.erir.view.DropListener;
import de.erb.erir.view.DropTargetPanel;
import de.erb.erir.view.ProgramLook;

/**
 * ERB-Orange: #FA7B1A
 *
 * @author David
 */
public class ResizerFrame extends JFrame implements ProgressListener<ConversionProgressEvent>,
		DropListener
{
	private static final long		serialVersionUID	= 4709736745642556840L;

	private static final String		VERSION_STRING		= "1.0";

	static final int				BORDER_SIZE			= 5;

	private static final int		WINDOW_WIDTH		= 200;
	private static final int		WINDOW_HEIGHT		= 100;

	private static final int		WINDOW_LOCATION_X	= 100;
	private static final int		WINDOW_LOCATION_Y	= 100;

	private ProgramLook				mProgramLook		= DefaultProgramLook.NEUTRAL;

	private DropTargetPanel			mDropTarget;
	private PlaceholderTextField	mWidthField;
	private PlaceholderTextField	mHeightField;
	private boolean					mConverting			= false;

	private Point					mDragStartPoint		= null;

	transient private KeyAdapter	mKeyListener;

	/**
	 * Listener that gets invoked whenever the user activates or deactivates an input field. Checks the input and colors
	 * the field background accordingly.
	 */
	transient private FocusAdapter	mFocusListener;

	/**
	 * Instanciates a new ResizerFrame.
	 *
	 * Look implementation defaults to DefaultProgramLook.NEUTRAL.
	 *
	 * @param pDecorated true to use the default OS window frame, false to omit it (the latter usually looks better)
	 */
	public ResizerFrame(boolean pDecorated)
	{
		mProgramLook = DefaultProgramLook.NEUTRAL;

		init(pDecorated);
	}

	/**
	 * Instanciates a new ResizerFrame.
	 *
	 * @param pDecorated true to use the default OS window frame, false to omit it (the latter usually looks better)
	 * @param pLook The program look implementation to use.
	 */
	public ResizerFrame(boolean pDecorated, ProgramLook pLook)
	{
		mProgramLook = pLook;

		init(pDecorated);
	}

	private void init(boolean pDecorated)
	{
		JPanel mainPanel = new JPanel(new GridLayout(1, 2));

		mKeyListener = new KeyAdapter()
		{
			@Override
			public void keyTyped(KeyEvent event)
			{
				boolean fail;
				JTextField tf = (JTextField) event.getComponent();
				try
				{
					PercentOrAbsoluteInteger value = PercentOrAbsoluteInteger.valueOf(tf.getText()
							+ event.getKeyChar());
					fail = value.getValue() <= 0;
				}
				catch(InputFormatException e)
				{
					// No valid number, maybe not finished yet
					fail = true;
				}

				tf.setBackground(fail ? mProgramLook.getFieldNeutralColor()
						: mProgramLook.getFieldOkColor());
			}
		};

		mFocusListener = new FocusAdapter()
		{
			@Override
			public void focusLost(FocusEvent event)
			{
				boolean fail;
				PlaceholderTextField tf = (PlaceholderTextField) event.getComponent();
				try
				{
					PercentOrAbsoluteInteger value = PercentOrAbsoluteInteger.valueOf(tf.getText());
					tf.setText(String.valueOf(value.getValue())
							+ (value.isPercentage() ? "%" : Messages.getString("ResizerFrame.pixel"))); //$NON-NLS-2$
					fail = value.getValue() <= 0;

					if(!fail)
					{
						PlaceholderTextField other = (event.getComponent() == mWidthField ? mHeightField
								: mWidthField);
						if(other.isEmpty())
						{
							// Fill the field appropriately
							if(value.isPercentage())
							{
								// Simply use the same value
								other.setText(tf.getText());
								other.setForeground(mProgramLook.getFieldTextColor());
								other.setBackground(mProgramLook.getFieldOkColor());
							}
						}
					}
				}
				catch(InputFormatException e)
				{
					// No valid number, maybe not finished yet
					fail = true;
				}
				tf.setBackground(fail ? mProgramLook.getFieldNeutralColor()
						: mProgramLook.getFieldOkColor());
			}

			@Override
			public void focusGained(FocusEvent event)
			{
				JTextField tf = (JTextField) event.getComponent();
				tf.setSelectionStart(0);
				tf.setSelectionEnd(tf.getText().length());
			}
		};

		mDropTarget = new DropTargetPanel(mProgramLook);
		mDropTarget.addDropListener(this);
		mDropTarget.setBackground(mProgramLook.getBackgroundColor());
		mDropTarget.setToolTipText(Messages.getString("ResizerFrame.dropToolTip"));

		// When the user drags the drop are, the window should be moved.
		mDropTarget.addMouseMotionListener(new MouseMotionAdapter()
		{
			@Override
			public void mouseDragged(MouseEvent event)
			{
				Point delta = new Point(event.getPoint().x - mDragStartPoint.x,
						event.getPoint().y - mDragStartPoint.y);
				Point newPos = new Point(getLocation().x + delta.x,
						getLocation().y + delta.y);
				setLocation(newPos);
			}
		});
		mDropTarget.addMouseListener(new MouseAdapter()
		{
			@Override
			public void mousePressed(MouseEvent event)
			{
				mDragStartPoint = event.getPoint();
			}
		});
		mDropTarget.setBorder(BorderFactory.createEmptyBorder(
				BORDER_SIZE, BORDER_SIZE, BORDER_SIZE, BORDER_SIZE));

		mainPanel.add(mDropTarget);

		// Create the right side settings panel
		{
			JPanel panel = new JPanel();
			panel.setBackground(mProgramLook.getBackgroundColor());
			panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
			panel.setBorder(BorderFactory.createEmptyBorder(
					BORDER_SIZE, BORDER_SIZE, BORDER_SIZE, BORDER_SIZE));

			mWidthField = new PlaceholderTextField(Messages.getString("ResizerFrame.widthPlaceholder")); //$NON-NLS-1$
			mWidthField.setSize(80, 40);
			mWidthField.setToolTipText(Messages.getString("ResizerFrame.widthTooltip")); //$NON-NLS-1$
			mWidthField.addKeyListener(mKeyListener);
			mWidthField.addFocusListener(mFocusListener);
			mWidthField.setForeground(mProgramLook.getFieldTextColor());
			mWidthField.setBackground(mProgramLook.getFieldNeutralColor());
			panel.add(mWidthField);

			mHeightField = new PlaceholderTextField(Messages.getString("ResizerFrame.heightPlaceholder")); //$NON-NLS-1$
			mHeightField.setSize(80, 40);
			mHeightField.addKeyListener(mKeyListener);
			mHeightField.addFocusListener(mFocusListener);
			mHeightField.setToolTipText(Messages.getString("ResizerFrame.heightTooltip")); //$NON-NLS-1$
			mHeightField.setForeground(mProgramLook.getFieldTextColor());
			mHeightField.setBackground(mProgramLook.getFieldNeutralColor());
			panel.add(mHeightField);

			JPanel bottomPanel = new JPanel();
			bottomPanel.setLayout(new BoxLayout(bottomPanel, BoxLayout.X_AXIS));
			bottomPanel.setBackground(mProgramLook.getBackgroundColor());

			JButton info = new JButton(mProgramLook.getInfoIcon());
			info.setBackground(mProgramLook.getForegroundColor());
			info.setForeground(mProgramLook.getFourgroundContrastColor());
			info.setToolTipText(Messages.getString("ResizerFrame.info")); //$NON-NLS-1$
			info.addActionListener(new ActionListener()
			{
				@Override
				public void actionPerformed(ActionEvent arg0)
				{
					// Display the Information Dialog.
					String message = Messages.getString("ResizerFrame.infoText");
					message = message.replaceAll("%VERSION%", VERSION_STRING);

					HtmlDialog.creator(ResizerFrame.this, message)
							.title(Messages.getString("ResizerFrame.infoHeadline"))
							.options(HtmlDialog.OPTION_OK)
							.size(700, 500)
							.show();
				}
			});
			bottomPanel.add(info);

			if(!pDecorated)
			{
				JButton close = new JButton(mProgramLook.getCloseIcon());

				close.setBackground(mProgramLook.getForegroundColor());
				close.setForeground(mProgramLook.getFourgroundContrastColor());
				close.addActionListener(new ActionListener()
				{
					@Override
					public void actionPerformed(ActionEvent arg0)
					{
						if(ResizerFrame.this.getDefaultCloseOperation() == JFrame.EXIT_ON_CLOSE)
						{
							System.exit(0);
						}
						else
						{
							ResizerFrame.this.dispose();
						}
					}
				});
				close.setToolTipText(Messages.getString("ResizerFrame.close")); //$NON-NLS-1$
				bottomPanel.add(close);
			}

			panel.add(bottomPanel);

			mainPanel.add(panel);
		}

		this.add(mainPanel);

		if(!pDecorated)
		{
			this.setUndecorated(true);
		}
		this.setIconImage(mProgramLook.getProgramImage());
		this.setTitle(Messages.getString("ResizerFrame.appName")); //$NON-NLS-1$
		this.setLocation(WINDOW_LOCATION_X, WINDOW_LOCATION_Y);
		this.setSize(WINDOW_WIDTH, WINDOW_HEIGHT);
		this.setVisible(true);
		this.setAlwaysOnTop(true);
	}

	/**
	 * Converts a given list of files (or folders).
	 *
	 * Changes the width and height of each image to the specified value.
	 *
	 * @param pFiles File list to convert. null is illegal and causes a NullPointerException.
	 */
	public synchronized void convertFiles(List<File> pFiles)
	{
		if(mConverting)
		{
			JOptionPane.showMessageDialog(null, Messages.getString("ResizerFrame.pleaseWait"), //$NON-NLS-1$
					Messages.getString("ResizerFrame.busy"), JOptionPane.INFORMATION_MESSAGE); //$NON-NLS-1$
			return;
		}

		// Step 1: Collect all files to convert.
		List<File> finalFileList = new ArrayList<File>();
		for(File file : pFiles)
		{
			collectFiles(file, finalFileList);
		}

		// Step 2: Check the user input
		PercentOrAbsoluteInteger destWidth = null;
		PercentOrAbsoluteInteger destHeight = null;

		if(!mWidthField.isEmpty())
		{
			try
			{
				destWidth = PercentOrAbsoluteInteger.valueOf(mWidthField.getText());
				if(destWidth.getValue() <= 0)
				{
					throw new InputFormatException();
				}
			}
			catch(InputFormatException e)
			{
				// User screwed the width up
				JOptionPane.showMessageDialog(null,
						Messages.getString("ResizerFrame.invalidWidth"), //$NON-NLS-1$
						Messages.getString("ResizerFrame.invalidInput"), JOptionPane.ERROR_MESSAGE); //$NON-NLS-1$
				return;
			}
		}

		if(!mHeightField.isEmpty())
		{
			try
			{
				destHeight = PercentOrAbsoluteInteger.valueOf(mHeightField.getText());
				if(destHeight.getValue() <= 0)
				{
					throw new InputFormatException();
				}
			}
			catch(InputFormatException e)
			{
				// User screwed the height up
				JOptionPane.showMessageDialog(null,
						Messages.getString("ResizerFrame.invalidHeight"), //$NON-NLS-1$
						Messages.getString("ResizerFrame.invalidInput"), JOptionPane.ERROR_MESSAGE); //$NON-NLS-1$
				return;
			}
		}

		// Both empty is not allowed.
		if(destWidth == null && destHeight == null)
		{
			JOptionPane.showMessageDialog(null,
					Messages.getString("ResizerFrame.missingInput"), //$NON-NLS-1$
					Messages.getString("ResizerFrame.missing"), JOptionPane.ERROR_MESSAGE); //$NON-NLS-1$
			return;
		}

		mConverting = true;

		// Step 3: Convert the files.
		ImageConversionWorker worker = new ImageConversionWorker(finalFileList, destWidth,
				destHeight);
		worker.addListener(mDropTarget);
		worker.addListener(this);
		Thread thread = new Thread(worker);
		thread.start();
	}

	/**
	 * Recursively collects all files out of the given directory.
	 *
	 * @param pStartFile If this is a start file, the method simply adds it to the list. If this is a directory, it
	 *            dives into it and recursively gets all files out of it.
	 * @param pTargetList Resulting list.
	 */
	private static void collectFiles(File pStartFile, List<File> pTargetList)
	{
		if(pStartFile.isFile())
		{
			pTargetList.add(pStartFile);
		}
		else if(pStartFile.isDirectory())
		{
			for(File subfile : pStartFile.listFiles())
			{
				collectFiles(subfile, pTargetList);
			}
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void onProgress(ConversionProgressEvent pEvent)
	{
		// Nothing to do here for the main window.
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void onOperationDone(ConversionProgressEvent pEvent)
	{
		mConverting = false;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void onOperationCancelled(ConversionProgressEvent pEvent)
	{
		mConverting = false;
		switch(pEvent.getStatusCode())
		{
			case IO_ERROR:
				JOptionPane.showMessageDialog(null,
						Messages.getString("ResizerFrame.ioError") + pEvent.getStatusMessage(), //$NON-NLS-1$
						Messages.getString("ResizerFrame.error"), JOptionPane.ERROR_MESSAGE); //$NON-NLS-1$
				break;

			case UNKNOWN_ERROR:
				JOptionPane.showMessageDialog(null,
						Messages.getString("ResizerFrame.unknownError"), //$NON-NLS-1$
						Messages.getString("ResizerFrame.error"), JOptionPane.ERROR_MESSAGE); //$NON-NLS-1$
				break;

			default:
				break;
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void onFileListDroppped(List<File> pFiles)
	{
		convertFiles(pFiles);
		repaint();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void onDragEntered()
	{
		repaint();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void onDragExited()
	{
		repaint();
	}

	public static void main(String[] args)
	{
		try
		{
			UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
		}
		catch(Exception e)
		{
			// If LnF fails, take no action since this is a noncritical error.
		}

		// Locale.setDefault(Locale.ENGLISH);

		// Use the nice, speech-bubble-shaped Tooltips.
		PopupFactory.setSharedInstance(new SpeechBubblePopupFactory());

		boolean decorated = args.length > 0 && args[0].equals("-frame");

		ResizerFrame frame = new ResizerFrame(decorated, DefaultProgramLook.ELEFANT_RACING);
		//ResizerFrame frame = new ResizerFrame(decorated, DefaultProgramLook.NEUTRAL);

		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	}
}
